

class ClsProperty(object):
    _obj_property_attrs = ['name', 'min', 'max', 'symbol', 'type', 'quiet', 'ignore_range']
    def __init__(self, **kwargs):
        self.cls = kwargs.get('cls')
        self.name = kwargs.get('name')
        self.ignore_type = kwargs.get('ignore_type', False)
        self.ignore_range = kwargs.get('ignore_range', False)
        self.default_value = kwargs.get('default')
        self.min = kwargs.get('min')
        self.max = kwargs.get('max')
        self.symbol = kwargs.get('symbol', '')
        self.type = kwargs.get('type', type(self.default_value))
        self.quiet = kwargs.get('quiet', False)
        
        for key in ['fget', 'fset', 'fvalidate', 'fformat']:
            fn = getattr(self, '_%s' % (key))
            attr = kwargs.get(key)
            if attr:
                fn = getattr(self.cls, attr)
            setattr(self, key, fn)
        
    def init_instance(self, obj):
        pkwargs = dict(zip(self._obj_property_attrs, [getattr(self, attr) for attr in self._obj_property_attrs]))
        pkwargs.update({'obj':obj, 'value':self.default_value})
        obj.Properties[self.name] = ObjProperty(**pkwargs)
        
    def _fget(self, obj):
        return obj.Properties[self.name].value
        
    def _fset(self, obj, value):
        value = self.fformat(obj, value)
        if self._validate_type(obj, value) and self.fvalidate(obj, value):
            obj.Properties[self.name].set_value(value)
            
    def _fvalidate(self, obj, value):
        prop = obj.Properties[self.name]
        if prop.ignore_range:
            return True
        if prop.min is not None and prop.max is not None:
            return value >= prop.min and value <= prop.max            
        return True
        
    def _fformat(self, obj, value):
        return value
        
    def _validate_type(self, obj, value):
        prop = obj.Properties[self.name]
        if self.ignore_type:
            return True
        if value is None:
            return True
        if prop.type is None:
            if value is not None:
                prop.type = type(value)
            return True
        return isinstance(value, prop.type)
        
    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        return self.fget(obj)
        
    def __set__(self, obj, value):
        #old = self.fget(obj)
        self.fset(obj, value)
        #if old != value:
        #    obj.Properties[self.name].emit(old=old)
        
class ObjProperty(object):
    __slots__ = ('name', 'value', 'min', 'max', 'symbol', 
                 'type', '_type', 'parent_obj', 'quiet', 
                 'ignore_range', 'own_callbacks', 'callbacks', 
                 'enable_emission', 'queue_emission')
    def __init__(self, **kwargs):
        self.enable_emission = True
        self.queue_emission = False
        self.name = kwargs.get('name')
        self.type = kwargs.get('type')
        self._type = EMULATED_TYPES.get(self.type)
        self.value = kwargs.get('value')
        if self._type is not None:
            self.value = self._type(self.value, parent_property=self)
        self.min = kwargs.get('min')
        self.max = kwargs.get('max')
        self.symbol = kwargs.get('symbol')
        self.parent_obj = kwargs.get('obj')
        self.quiet = kwargs.get('quiet')
        self.ignore_range = kwargs.get('ignore_range')
        self.own_callbacks = set()
        self.callbacks = set()
    
    @property
    def range(self):
        return [self.min, self.max]
    @range.setter
    def range(self, value):
        self.min, self.max = value
        
    def set_value(self, value):
        if self._type is not None:
            old = self.value.copy()
        else:
            old = self.value
        self.enable_emission = False
        if self._type is not None:
            self.value._update_value(value)
        else:
            self.value = value
        self.enable_emission = True
        
        if old != self.value or self.queue_emission:
            self.emit(old)
        self.queue_emission = False
            
    def bind(self, cb):
        if getattr(cb, 'im_self', None) == self.parent_obj:
            self.own_callbacks.add(cb)
        else:
            self.callbacks.add(cb)
            
    def unbind(self, cb):
        self.callbacks.discard(cb)
        self.own_callbacks.discard(cb)
        
    def emit(self, old):
        if not self.enable_emission:
            self.queue_emission = True
            return
        value = getattr(self.parent_obj, self.name)
        cb_kwargs = dict(name=self.name, Property=self, value=value, old=old, obj=self.parent_obj)
        for cb in self.own_callbacks:
            cb(**cb_kwargs)
        for cb in self.callbacks:
            cb(**cb_kwargs)
        if not self.quiet:
            self.parent_obj.emit('property_changed', **cb_kwargs)

class ListProperty(list):
    def __init__(self, initlist=None, **kwargs):
        self.parent_property = kwargs.get('parent_property')
        super(ListProperty, self).__init__(initlist)
    def _update_value(self, value):
        for i, item in enumerate(value):
            if i <= len(self):
                if item != self[i]:
                    self[i] = item
            else:
                self.append(item)
    def __setitem__(self, i, item):
        old = self[:]
        list.__setitem__(self, i, item)
        self.parent_property.emit(old)
    def __delitem__(self, i):
        old = self[:]
        list.__delitem__(self, i)
        self.parent_property.emit(old)
    def append(self, *args):
        old = self[:]
        super(ListProperty, self).append(*args)
        self.parent_property.emit(old)
    def insert(self, *args):
        old = self[:]
        super(ListProperty, self).insert(*args)
        self.parent_property.emit(old)
    def pop(self, *args):
        old = self[:]
        super(ListProperty, self).pop(*args)
        self.parent_property.emit(old)
    def remove(self, *args):
        old = self[:]
        super(ListProperty, self).remove(*args)
        self.parent_property.emit(old)
    def extend(self, *args):
        old = self[:]
        super(ListProperty, self).extend(*args)
        self.parent_property.emit(old)
        
class DictProperty(dict):
    def __init__(self, initdict=None, **kwargs):
        self.parent_property = kwargs.get('parent_property')
        super(DictProperty, self).__init__(initdict)
    def _update_value(self, value):
        self.update(value)
    def __setitem__(self, key, item):
        change = self._check_for_change(key, item)
        dict.__setitem__(self, key, item)
        if change:
            self.parent_property.emit(None)
    def __delitem__(self, key):
        dict.__delitem__(self, key)
        self.parent_property.emit(None)
    def clear(self, *args):
        super(DictProperty, self).clear(*args)
        self.parent_property.emit(None)
    def update(self, d):
        for key, val in d.iteritems():
            if self._check_for_change(key, val):
                self.parent_property.queue_emission = True
        super(DictProperty, self).update(d)
    def _check_for_change(self, key, value):
        if key not in self:
            return True
        return value != self[key]
        
EMULATED_TYPES = {list:ListProperty, dict:DictProperty}
    
class PropertyConnector(object):
    '''
    Mixin for objects to easily connect to Properties
    
    '''
    @property
    def Property(self):
        if not hasattr(self, '_Property'):
            self._Property = None
        return self._Property
    @Property.setter
    def Property(self, value):
        if type(value) == tuple or type(value) == list:
            obj, propname = value
            value = obj.Properties[propname]            
        if value != self.Property:
            if self.Property is not None:
                self.unlink_Property(self.Property)
            self._Property = value
            if value is not None:
                self.attach_Property(value)
    def unlink_Property(self, prop):
        prop.unbind(self.on_Property_value_changed)
    def attach_Property(self, prop):
        prop.bind(self.on_Property_value_changed)
    def set_Property_value(self, value, convert_type=False):
        if self.Property is not None:
            if convert_type:
                value = self.Property.type(value)
            setattr(self.Property.parent_obj, self.Property.name, value)
    def get_Property_value(self):
        if self.Property is not None:
            return getattr(self.Property.parent_obj, self.Property.name)
    def on_Property_value_changed(self, **kwargs):
        '''
        Override this method to get Property updates
        '''
        pass
